<?php
use WeChatPay\Crypto\Rsa;
use WeChatPay\Util\PemUtil;
use WeChatPay\Builder;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Redis;
use WeChatPay\Crypto\AesGcm;
use WeChatPay\Formatter;

class WxPay{
    public $mchid = '';
    public $apikey = '';
    public $serialno = '';
    public $appid = '';
    public $appsecret = '';
    public $pvkey = '';
    public $ISDEBUG = false;
    function __construct($paycfg){
        $this->mchid = $paycfg['mchid'];
        $this->apikey = $paycfg['apikey'];
        $this->serialno = $paycfg['serialno'];
        $this->appid = $paycfg['appid'];
        $this->appsecret = $paycfg['appsecret'];
        $this->pvkey = $paycfg['pvkey'];
        if (isset($paycfg['isdebug'])) $this->ISDEBUG = true;
    }
    
    public function getWxToken(){
        $token = Redis::get('superbar_access_token');
        if($token) return $token;
        $appid = trim($this->appid);
        $secret = trim($this->appsecret);
        $url = "https://api.weixin.qq.com/cgi-bin/stable_token";
        $res = curPost($url,json_encode(array('grant_type'=>'client_credential','appid'=>$appid,'secret'=>$secret)));
        Redis::setex('superbar_access_token',$res['expires_in'], $res['access_token']);
        return $res['access_token'];
    }
    
    public function getJsTicket($url){
        $token = Redis::get("superbar_wxjstoken");
        if (empty($token)){
            $url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=".$this->getWxToken()."&type=jsapi";
            $res = curPost($url);
            if(!isset($res['ticket'])) return [];
            Redis::setex("superbar_wxjstoken",$res['expires_in'],$res['ticket']);
            $token = $res['ticket'];
        }
        
        $data = array(
                'noncestr'=>random(10),
                'jsapi_ticket'=>$token,
                'timestamp'=>time(),
                'url'=>$url,
        );
        ksort($data);
        $sign = sha1(toUrlParams($data));
        return array(
                'appId' => $this->appid,
                'timestamp'=>$data['timestamp'],
                'nonceStr' => $data['noncestr'],
                'signature' => $sign,
        );
    }
    
    public function getQrTicket(){
        $ticket = Redis::get("superbar_platqrticket");
        if ($ticket) return $ticket;
        $url = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=".$this->getWxToken();
        $barcode = array(
                'action_info'=>array(
                        'scene'=>array(
                                'scene_str'=>"superbar",
                        )
                ),
                'action_name'=>'QR_LIMIT_STR_SCENE'
        );
        $res = curPost($url,json_encode($barcode));
        if (empty($res['ticket'])) return resp(0,'生成失败');
        Redis::setex('superbar_platqrticket',24*3600, $res['ticket']);
        return $res['ticket'];
    }
    
    public function wxappPay($params){
        try {
            if ($this->ISDEBUG) $params['price'] = 0.01;
            $post = json_encode(array(
                    'appid'=>$this->appid,
                    'mchid'=>$this->mchid,
                    'description'=>$params['body'],
                    'out_trade_no'=>$params['orderno'],
                    'attach'=>$params['attach'] ?? '',
                    'notify_url'=>$params['notify_url'],
                    'amount'=>array(
                            'total'=>round($params['price'] * 100,2),
                            'currency'=>'CNY'
                    ),
                    'payer'=>array('openid'=>$params['openid'])
            ),JSON_UNESCAPED_UNICODE);
            $res = $this->wxPost('v3/pay/transactions/jsapi',array(
                    'body'=>$post
            ));
        } catch (\Exception $e) {
            return array('code'=>0,'msg'=>$e->getMessage());
        }
        if (isset($res['prepay_id'])){
            $merchantPrivateKeyInstance = Rsa::from($this->pvkey, Rsa::KEY_TYPE_PRIVATE);
            $paydata = array(
                    'appid'=>$this->appid,
                    'timeStamp'=>(string)Formatter::timestamp(),
                    'nonceStr'=>Formatter::nonce(),
                    'package'=>'prepay_id='.$res['prepay_id'],
            );
            $paydata['paySign'] = Rsa::sign(Formatter::joinedByLineFeed(...array_values($paydata)),$merchantPrivateKeyInstance);
            $paydata['signType'] = 'RSA';
            return array('code'=>1,'paydata'=>$paydata);
        }else return array('code'=>0,'msg'=>json_encode($res));
    }
    
    public function scanPay($params){
        try {
            $res = $this->wxPost('v3/pay/transactions/native',array(
                    'body'=>json_encode(array(
                            'appid'=>$this->appid,
                            'mchid'=>$this->mchid,
                            'description'=>$params['body'],
                            'out_trade_no'=>$params['orderno'],
                            'attach'=>$params['attach'] ?? '',
                            'notify_url'=>$params['notify_url'],
                            'amount'=>array(
                                    'total'=>$params['price'] * 100,
                                    'currency'=>'CNY'
                            )
                    ),JSON_UNESCAPED_UNICODE)
            ));
        } catch (\Exception $e) {
            return array('code'=>0,'msg'=>$e->getMessage());
        }
        return array('code'=>1,'url'=>$res['code_url']);
    }
    
    public function refund($transid,$price,$reason = ''){
        try {
            $result = $this->checkOrder($transid);
            $total = $result['amount']['total'];
            if ($this->ISDEBUG){
                $price = 0.01;
                $total = 1;
            }
            $post = array(
                    'transaction_id'=>$transid,
                    'out_refund_no'=>"RD".date('YmdHis').random(6,1),
                    'amount'=>array(
                            'refund'=>intval(round($price,2) * 100),
                            'total'=> $total,
                            'currency'=>'CNY'
                    ),
            );
            if ($reason) $post['reason'] = $reason;
            Log::build(['driver' => 'single','path' => storage_path('logs/refund/'.date('Ymd'))])->info(json_encode($post));
            $res = $this->wxPost('v3/refund/domestic/refunds',array(
                    'body'=>json_encode($post)
            ));
            if (isset($res['refund_id'])) return array('code'=>1,'transid'=>$res['refund_id']);
            Log::build(['driver' => 'single','path' => storage_path('logs/refund/'.date('Ymd'))])->info(json_encode($res));
        } catch (\Exception $e) { 
            Log::build(['driver' => 'single','path' => storage_path('logs/refund/'.date('Ymd'))])->info($e->getMessage());
        }
        return array('code'=>0);
    }
    
    public  function handleNotify($inBody){
        // 转换通知的JSON文本消息为PHP Array数组
        $inBodyArray = (array)json_decode($inBody, true);
        // 使用PHP7的数据解构语法，从Array中解构并赋值变量
        ['resource' => [
                'ciphertext'      => $ciphertext,
                'nonce'           => $nonce,
                'associated_data' => $aad
        ]] = $inBodyArray;
        // 加密文本消息解密
        $inBodyResource = AesGcm::decrypt($ciphertext, $this->apikey, $nonce, $aad);
        // 把解密后的文本转换为PHP Array数组
        $result =  (array)json_decode($inBodyResource, true);
        $order = $this->checkOrder($result['transaction_id']);
        if (!isset($order['transaction_id'])) return [];
        if ($result['amount']['total'] != $order['amount']['total']) return [];
        $result['price'] = $result['amount']['total'];
        return $this->getValues($result, ['transaction_id','price','out_trade_no','attach']);
    }
    
    private function getValues($arr,$cols){
        $narr = [];
        foreach ($cols as $key) {
            $narr[$key] = $arr[$key]??'';
        }
        return $narr;
    }
    
    public function checkOrder($transid){
        return $this->wxPost("v3/pay/transactions/id/{$transid}?mchid=".$this->mchid);
    }
    
    public function wxPost($action,$params = array(),$needheader = false,$put = false){
        $merchantId = $this->mchid;
        $this->dealPayCert();
        // 从本地文件中加载「商户API私钥」，「商户API私钥」会用来生成请求的签名
        $merchantPrivateKeyInstance = Rsa::from($this->pvkey, Rsa::KEY_TYPE_PRIVATE);
        // 从本地文件中加载「微信支付平台证书」，用来验证微信支付应答的签名
        $platformCertificateFilePath = 'file://'.Storage::path('superbar_wxpay_pubkey');
        
        // 「商户API证书」的「证书序列号」
        $merchantCertificateSerial = $this->serialno;
        
        $platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
        
        // 从「微信支付平台证书」中获取「证书序列号」
        $platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);
        // 构造一个 APIv3 客户端实例
        $instance = Builder::factory([
                'mchid'      => $merchantId,
                'serial'     => $merchantCertificateSerial,
                'privateKey' => $merchantPrivateKeyInstance,
                'certs'      => [
                        $platformCertificateSerial => $platformPublicKeyInstance,
                ],
        ]);
        if ($needheader) $params['headers'] = ['Wechatpay-Serial' => $platformCertificateSerial];
        // 发送请求
        $instance = $instance->chain($action);
        if ($params){
            if ($put) $instance = $instance->put($params);
            else $instance = $instance->post($params);
        }else $instance = $instance->get();
        return json_decode($instance->getBody(),true);
    }

    //下载 微信支付平台证书
    private function dealPayCert(){
        if (Storage::get('superbar_wxpay_pubkey')) return;
        $nonce = random(32);
        $time = time();
        $url = 'https://api.mch.weixin.qq.com/v3/certificates';
        $str = "GET\n/v3/certificates\n{$time}\n{$nonce}\n\n";
        try {
            openssl_sign($str, $raw_sign, openssl_pkey_get_private($this->pvkey),'sha256WithRSAEncryption');
            $sign = base64_encode($raw_sign);
            $schema = 'WECHATPAY2-SHA256-RSA2048';
            $token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
                    $this->mchid, $nonce, $time, $this->serialno, $sign);
            
            $curl = curl_init();
            curl_setopt($curl, CURLOPT_URL, $url);
            curl_setopt($curl, CURLOPT_HEADER, 0);
            curl_setopt($curl, CURLOPT_HTTPHEADER, array('Authorization: '.$schema." ".$token));
            curl_setopt($curl,CURLOPT_USERAGENT,"1");
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); // 对认证证书来源的检查
            curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0); // 从证书中检查SSL加密算法是否存在
            $data = curl_exec($curl);
            curl_close($curl);
            $result = json_decode($data,true);
            if (isset($result['data'])){
                $data = $result['data'][0]['encrypt_certificate'];
                Storage::put("superbar_wxpay_pubkey",AesGcm::decrypt($data['ciphertext'],$this->apikey,$data['nonce'],$data['associated_data']));
            }
        } catch (\Exception $e) {
            return array('code'=>0,'msg'=>$e->getMessage());
        }
    }
}
